﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Fabrication.Exceptions;
using Fabrication.ValueProviders;

namespace Fabrication
{
    internal class BuilderOptions : IBuilderOptions
    {
        private readonly ConventionsProvider conventionsProvider;
        private readonly List<ILinkDetails> allLinkDetails;
        private readonly List<DependencyDetails> allDependencies;
        private readonly Dictionary<TypedPropertyKey, IValueProvider> generatedValueProviders;

        protected BuilderOptions(ConventionsProvider conventionsProvider)
        {
            this.conventionsProvider = conventionsProvider;
            generatedValueProviders = new Dictionary<TypedPropertyKey, IValueProvider>();
            allLinkDetails = new List<ILinkDetails>();
            allDependencies = new List<DependencyDetails>();
        }

        public void AddPropertyValueProvider(TypedPropertyKey propertyName, IValueProvider valueProvider)
        {
            if (generatedValueProviders.ContainsKey(propertyName))
                throw new AmbiguousRuleException(string.Format("Property '{0}' has a previous rule defined.", propertyName));

            generatedValueProviders.Add(propertyName, (valueProvider));
        }

        public bool TryGetValue(object instance, PropertyInfo property, out object value)
        {
            var valueProvider = GetProviderForProperty(instance.GetType(), property);
            if (valueProvider != null)
            {
                value = valueProvider.GetValue(instance);
                return true;
            }
            value = null;
            return false;
        }

        public void AddChildLink(Type parentType, Type childType, Type parameterType, string parentProperty, string childProperty)
        {
            var linkDetails = new LinkDetails(parentType, childType, parameterType, parentProperty, parameterType, childProperty);
            AddLinkDetails(linkDetails);
        }

        public bool TryGetSourceLinkDetails(PropertyInfo targetProperty, out ILinkDetails linkDetails)
        {
            linkDetails = allLinkDetails.SingleOrDefault(ld => ld.TargetMatches(targetProperty));
            return linkDetails != null;
        }

        public void AddInternalLink(Type owningType, Type type, string sourceProperty, string targetProperty)
        {
            var linkDetails = new LinkDetails(owningType, owningType, type, sourceProperty, targetProperty);
            AddLinkDetails(linkDetails);
        }

        public void AddDependencies(Type owningType, Type propertyType, string targetProperty, IEnumerable<string> dependencies)
        {
            var dependencyDetails = new DependencyDetails(owningType, propertyType, targetProperty, dependencies);
            allDependencies.Add(dependencyDetails);
        }

        public void AddMap<TSource, TTarget>(Type owningType, string sourcePropertyName, string targetPropertyName, IDictionary<TSource, TTarget> lookup)
        {
            AddMap<TSource, TTarget>(owningType, sourcePropertyName, targetPropertyName, x => lookup[x]);
        }

        public void AddMap<TSource, TTarget>(Type owningType, string sourcePropertyName, string targetPropertyName, Func<TSource, TTarget> modifier)
        {
            if (sourcePropertyName == targetPropertyName)
                throw new LinkedPropertyException(string.Format("Cannot map property '{0}' to itself.", sourcePropertyName));

            GuardAgainstCircularReference<TSource, TTarget>(owningType, sourcePropertyName, targetPropertyName);

            var linkDetails = new LinkDetails(owningType, owningType, typeof(TSource), sourcePropertyName, typeof(TTarget), targetPropertyName, x => modifier((TSource)x));
            AddLinkDetails(linkDetails);
        }

        public void AddMap<TSource1, TSource2, TTarget>(Type owningType, string sourcePropertyName1, string sourcePropertyName2, string targetPropertyName, Func<TSource1, TSource2, TTarget> modifier)
        {
            if (sourcePropertyName1 == targetPropertyName || sourcePropertyName2 == targetPropertyName)
                throw new LinkedPropertyException(string.Format("Cannot map property '{0}' to itself.", targetPropertyName));

            GuardAgainstCircularReference<TSource1, TTarget>(owningType, sourcePropertyName1, targetPropertyName);
            GuardAgainstCircularReference<TSource2, TTarget>(owningType, sourcePropertyName2, targetPropertyName);

            var linkDetails = new DoubleLinkDetails(owningType, owningType, new TypedPropertyKey(typeof(TSource1), sourcePropertyName1), new TypedPropertyKey(typeof(TSource2), sourcePropertyName2), new TypedPropertyKey(typeof(TTarget), targetPropertyName), (x,y) => modifier((TSource1)x, (TSource2)y));
            AddLinkDetails(linkDetails);
        }

        private void GuardAgainstCircularReference<TSource, TTarget>(Type owningType, string sourcePropertyName, string targetPropertyName)
        {
            var existingSourceLinks = GetAllLinksWhereSourceIs(targetPropertyName, typeof (TTarget), owningType);
            foreach (var existingSourceLink in existingSourceLinks)
            {
                if (existingSourceLink.TargetMatches(sourcePropertyName, typeof (TSource), owningType))
                    throw new LinkedPropertyException(string.Format("Cannot map property '{0}' to property '{1}' as this creates a circular dependency.", sourcePropertyName, targetPropertyName));

                GuardAgainstCircularReference<TSource, TTarget>(owningType, sourcePropertyName, existingSourceLink.TargetPropertyName);
            }
        }

        public void AddEnsureRule<TSource, TTarget>(Type owningType, string sourcePropertyName, string targetPropertyName, Func<TSource, TTarget> modifier)
        {
            if (sourcePropertyName == targetPropertyName)
                throw new EnsurePropertyException(string.Format("Cannot ensure property '{0}' against itself", sourcePropertyName));

            var linkDetails = new LinkDetails(owningType, owningType, typeof(TSource), sourcePropertyName, targetPropertyName, x => modifier((TSource)x));
            AddLinkDetails(linkDetails);
        }

        public IEnumerable<ILinkDetails> GetAllLinksWhereSourceIs(PropertyInfo property)
        {
            return allLinkDetails.Where(ld => ld.SourceMatches(property));
        }

        public IEnumerable<ILinkDetails> GetAllLinksWhereSourceIs(string propertyName, Type propertyType, Type owningType)
        {
            return allLinkDetails.Where(ld => ld.SourceMatches(propertyName, propertyType, owningType));
        }

        public IEnumerable<string> GetDependenciesFor(PropertyInfo property)
        {
            var details = allDependencies.Where(d => d.Matches(property));
            if (!details.Any())
                return new List<string>();

            return details.SelectMany(x => x.Dependencies);
        }

        private void AddLinkDetails(ILinkDetails linkDetails)
        {
            var targetAlreadyExists = allLinkDetails.Any(ld => ld.TargetMatches(linkDetails));
            if (targetAlreadyExists)
                throw new LinkedPropertyException(string.Format("Property '{0}' is already a mapped target.", linkDetails.TargetPropertyName));

            allLinkDetails.Add(linkDetails);
        }

        private IValueProvider GetProviderForProperty(Type instanceType, PropertyInfo property)
        {
            var propertyKey = new TypedPropertyKey(property.DeclaringType, property.Name);

            IValueProvider valueProvider;

            // If a rule has previously been generated for this property then use it
            if (generatedValueProviders.TryGetValue(propertyKey, out valueProvider))
                return valueProvider;

            if (conventionsProvider.TryGetConventionFor(property, out valueProvider))
            {
                valueProvider.Initialize(property);
                generatedValueProviders.Add(propertyKey, valueProvider);
                return valueProvider;
            }

            return null;
        }

        public void ResetProviders()
        {
            foreach (var provider in generatedValueProviders.Values.OfType<IResettableProvider>())
                provider.Reset();
        }
    }

    internal class BuilderOptions<T> : BuilderOptions, IBuilderOptions<T>
    {
        public BuilderOptions(ConventionsProvider conventionsProvider) 
            : base(conventionsProvider) { }

        public Func<T> ConstructionMethod { get; private set; }

        public void SetConstructionMethod(Func<T> constructionMethod)
        {
            ConstructionMethod = constructionMethod;
        }
    }
}
